Банки — Анализ оттока клиентов Необходимо проанализировать клиентов регионального банка «Метанпром» и выделить сегменты клиентов, которые склонны уходить из банка.
Датасет содержит данные о клиентах банка «Метанпром». Банк располагается в Ярославле и областных городах: Ростов Великий и Рыбинск.
Колонки:
userid — идентификатор пользователя,score — баллы кредитного скоринга,City — город,Gender — пол,Age — возраст,Equity — приблизительная оценка собственности клиента,Balance — баланс на счёте,Products — количество продуктов, которыми пользуется клиент,CreditCard — есть ли кредитная карта,last_activity — активность клиента 1(активный)/0(неактивный),est_salary — заработная плата клиента,Churn — ушёл или нет.2.1 Приведение заголовков столбцов к "змеиному" регистру
2.3 Обработка пропущенных значений
2.5 Обработка аномальных значений
2.6 Создание столбца с категорией города (цифра соответствует определённому городу)
3.1 Определить зависимость баллов кредитного скоринга и вероятности ухода
3.2 Определить зависимость вероятности ухода от города
3.3 Какого пола клиенты чаще уходят
3.4 Определить зависимость вероятности ухода от возраста
3.5 Определить зависимость вероятности ухода от оценки собственности клиента
3.6 Определить зависимость вероятности ухода от баланса на счёте
3.7 Определить зависимость вероятности ухода от количества продуктов, которыми пользуется клиент
3.8 Определить зависимость вероятности ухода от наличия кредитной карты
3.9 Определить зависимость вероятности ухода от активности
3.10 Определить зависимость вероятности ухода от заработной платы
3.11 Произвести анализ вероятности ухода в зависимости сразу от нескольких критериев
3.12 Составить портрет клиента, склонного уходить
3.13 Выводы
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats as st
import plotly.figure_factory as ff
import plotly.express as px
from plotly import graph_objects as go
from plotly.subplots import make_subplots
path = "https://drive.google.com/uc?export=download&id=1-U61mhTz_N1ARjy2XSAZ7IlQqGjeqP0F"
df = pd.read_csv(path)
df.head(5)
| USERID | score | city | gender | age | equity | balance | products | credit_card | last_activity | EST_SALARY | churn | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 183012 | 850.0 | Рыбинск | Ж | 25 | 1 | 59214.82 | 2 | 0 | 1 | 75719.14 | 1 |
| 1 | 146556 | 861.0 | Рыбинск | Ж | 37 | 5 | 850594.33 | 3 | 1 | 0 | 86621.77 | 0 |
| 2 | 120722 | 892.0 | Рыбинск | Ж | 30 | 0 | NaN | 1 | 1 | 1 | 107683.34 | 0 |
| 3 | 225363 | 866.0 | Ярославль | Ж | 51 | 5 | 1524746.26 | 2 | 0 | 1 | 174423.53 | 1 |
| 4 | 157978 | 730.0 | Ярославль | М | 34 | 5 | 174.00 | 1 | 1 | 0 | 67353.16 | 1 |
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 10000 entries, 0 to 9999 Data columns (total 12 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 USERID 10000 non-null int64 1 score 10000 non-null float64 2 city 10000 non-null object 3 gender 10000 non-null object 4 age 10000 non-null int64 5 equity 10000 non-null int64 6 balance 7705 non-null float64 7 products 10000 non-null int64 8 credit_card 10000 non-null int64 9 last_activity 10000 non-null int64 10 EST_SALARY 10000 non-null float64 11 churn 10000 non-null int64 dtypes: float64(3), int64(7), object(2) memory usage: 937.6+ KB
df.describe()
| USERID | score | age | equity | balance | products | credit_card | last_activity | EST_SALARY | churn | |
|---|---|---|---|---|---|---|---|---|---|---|
| count | 10000.00000 | 10000.000000 | 10000.000000 | 10000.000000 | 7.705000e+03 | 10000.000000 | 10000.000000 | 10000.000000 | 1.000000e+04 | 10000.000000 |
| mean | 171814.71260 | 848.699400 | 42.837100 | 2.627600 | 8.277943e+05 | 1.874100 | 0.680400 | 0.523500 | 1.478669e+05 | 0.182200 |
| std | 33708.23812 | 65.448519 | 12.128507 | 1.980836 | 1.980614e+06 | 0.799946 | 0.466345 | 0.499472 | 1.393885e+05 | 0.386029 |
| min | 94561.00000 | 642.000000 | 18.000000 | 0.000000 | 0.000000e+00 | 0.000000 | 0.000000 | 0.000000 | 2.546300e+03 | 0.000000 |
| 25% | 142810.25000 | 802.000000 | 34.000000 | 0.000000 | 2.955542e+05 | 1.000000 | 0.000000 | 0.000000 | 7.525190e+04 | 0.000000 |
| 50% | 172728.00000 | 853.000000 | 40.000000 | 3.000000 | 5.242722e+05 | 2.000000 | 1.000000 | 1.000000 | 1.196581e+05 | 0.000000 |
| 75% | 201261.75000 | 900.000000 | 51.000000 | 4.000000 | 9.807058e+05 | 2.000000 | 1.000000 | 1.000000 | 1.745005e+05 | 0.000000 |
| max | 229145.00000 | 1000.000000 | 86.000000 | 9.000000 | 1.191136e+08 | 5.000000 | 1.000000 | 1.000000 | 1.395064e+06 | 1.000000 |
df.hist(figsize=(15,20));
В датасете 10000 строк и 12 столбцов. Только в столбце balance есть пропуски. Каких-то аномальных данных из "описания" пока не видно.
df.columns = df.columns.str.lower()
df.columns
Index(['userid', 'score', 'city', 'gender', 'age', 'equity', 'balance',
'products', 'credit_card', 'last_activity', 'est_salary', 'churn'],
dtype='object')
Имеет смысл у столбца score изменить тип данных с float64 на uint16 (целые числа в диапазоне от 0 по 65535) т.к. это целые значения от 0 до 1000.
Также у столбцов age, equity, products, credit_card, last_activity, churn изменить тип с int64 на uint8 (целые числа в диапазоне от 0 по 255) для ускорения расчётов и экономии оперативной памяти.
df[['age','equity', 'products', 'credit_card', 'last_activity', 'churn']] = \
df[['age','equity', 'products', 'credit_card', 'last_activity', 'churn']] .astype('uint8')
df['score'] = df['score'].astype('uint16')
#проверим данные на наличие пропусков
print(df.isna().sum())
userid 0 score 0 city 0 gender 0 age 0 equity 0 balance 2295 products 0 credit_card 0 last_activity 0 est_salary 0 churn 0 dtype: int64
fig = px.histogram(df, x="balance", color="churn", marginal="rug",
hover_data=df.columns, title='Распределение данных столбца balance')
fig.update_layout(xaxis_title='Баланс счёта клиента')
fig.show()
Распределение данных скошено вправо в сторону высоких значений баланса, поэтому заполнять пропуски средним значением будет ошибкой. Заполним пропуски медианным значением балланса, медиану посчитаем у уходящих и остающихся клиентов отдельно, предполагая, что есть разница в балансе, что подтверждает график выше.
Проверим уникальные названия городов, если есть повторяющиеся с разными регистрами, приведём их к одному
df['city'].unique()
array(['Рыбинск', 'Ярославль', 'Ростов'], dtype=object)
С городами всё в порядке. Проверим тоже самое с в столбуе gender
df['gender'].unique()
array(['Ж', 'М'], dtype=object)
Тоже всё в порядке
#проверим количесво дубликатов
df.duplicated().sum()
1
# удалим дубликат и убедимся, что удалили его
df = df[df.duplicated()!=True]
df.duplicated().sum()
0
df.describe()
| userid | score | age | equity | balance | products | credit_card | last_activity | est_salary | churn | |
|---|---|---|---|---|---|---|---|---|---|---|
| count | 9999.000000 | 9999.000000 | 9999.000000 | 9999.000000 | 7.705000e+03 | 9999.000000 | 9999.000000 | 9999.000000 | 9.999000e+03 | 9999.000000 |
| mean | 171817.699870 | 848.691369 | 42.838084 | 2.627863 | 8.277943e+05 | 1.874187 | 0.680368 | 0.523552 | 1.478435e+05 | 0.182218 |
| std | 33708.600055 | 65.446864 | 12.128714 | 1.980761 | 1.980614e+06 | 0.799938 | 0.466357 | 0.499470 | 1.393758e+05 | 0.386044 |
| min | 94561.000000 | 642.000000 | 18.000000 | 0.000000 | 0.000000e+00 | 0.000000 | 0.000000 | 0.000000 | 2.546300e+03 | 0.000000 |
| 25% | 142815.500000 | 802.000000 | 34.000000 | 0.000000 | 2.955542e+05 | 1.000000 | 0.000000 | 0.000000 | 7.525178e+04 | 0.000000 |
| 50% | 172740.000000 | 853.000000 | 40.000000 | 3.000000 | 5.242722e+05 | 2.000000 | 1.000000 | 1.000000 | 1.196547e+05 | 0.000000 |
| 75% | 201262.500000 | 900.000000 | 51.000000 | 4.000000 | 9.807058e+05 | 2.000000 | 1.000000 | 1.000000 | 1.744997e+05 | 0.000000 |
| max | 229145.000000 | 1000.000000 | 86.000000 | 9.000000 | 1.191136e+08 | 5.000000 | 1.000000 | 1.000000 | 1.395064e+06 | 1.000000 |
Аномальные значения могут встретиться в столбцах age, balance и est_salary. Построим по ним диаграммы размаха.
plt.title("Диаграмма размаха данных столбца 'age'")
ax=sns.boxplot(x = df['age'])
ax.set(xlabel='Возраст клиентов')
plt.show()
Старше 77 лет уже не совсем типичные клиенты банка.
plt.title("Диаграмма размаха данных столбца 'balance'")
ax=sns.boxplot(x = df['balance'])
ax.set(xlabel='Баланс счёта')
plt.show()
Данные сильно смещены, очень много выбросов, которые могут искажать картину. Чтобы немного выровнять имеет смысл удалить пользователей с балансом выше 20 млн.
plt.title("Диаграмма размаха данных столбца 'est_salary'")
ax=sns.boxplot(x = df['est_salary'])
ax.set(xlabel='Предполагаемый ежемесячный доход')
plt.show()
Данные тоже смещены. Все данные "за усами" исключить нельзя, т.к. мы лишаемся большого количества данных. Попробуем взять границу в 600 тыс.
Посчитаем какое количество данных мы удалим, применяя критерии выше.
print(round(len(df[(df['balance'] > 20000000) | (df['est_salary'] > 600000) | (df['age'] > 77)])/len(df['userid'])*100,2), '%')
2.62 %
2.62% это приемлемое количество данных для удаления (<5). Удаляем
index= df[(df['balance'] > 20000000) | (df['est_salary']> 600000) |(df['age'] > 77)].index.to_list()
df1 = df.drop(index = index)
len(df1)
9737
df =df1.copy()
df2 = pd.get_dummies (df, columns=['city', 'gender'], drop_first= False)
df2.head()
| userid | score | age | equity | balance | products | credit_card | last_activity | est_salary | churn | city_Ростов | city_Рыбинск | city_Ярославль | gender_Ж | gender_М | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 183012 | 850 | 25 | 1 | 59214.82 | 2 | 0 | 1 | 75719.14 | 1 | 0 | 1 | 0 | 1 | 0 |
| 1 | 146556 | 861 | 37 | 5 | 850594.33 | 3 | 1 | 0 | 86621.77 | 0 | 0 | 1 | 0 | 1 | 0 |
| 2 | 120722 | 892 | 30 | 0 | NaN | 1 | 1 | 1 | 107683.34 | 0 | 0 | 1 | 0 | 1 | 0 |
| 3 | 225363 | 866 | 51 | 5 | 1524746.26 | 2 | 0 | 1 | 174423.53 | 1 | 0 | 0 | 1 | 1 | 0 |
| 4 | 157978 | 730 | 34 | 5 | 174.00 | 1 | 1 | 0 | 67353.16 | 1 | 0 | 0 | 1 | 0 | 1 |
df = df2.copy()
cm = df.corr()
fig, ax = plt.subplots(figsize = (14, 14))
sns.heatmap(cm, annot = True, square=True, cmap="plasma")
plt.title('Матрица корреляций')
plt.show()
Согласно матрице наибольшая связь с оттоком клиентов наблюдается у следующих параметров:
оценка собственности клиента (equity)
баланс счёта (balance)
количество используемых продуктов банка (products)
Для определения зависимости баллов кредитного скоринга (кредитный рейтинг) и вероятности уходя сделаем следующие действия:
посчитаем коэфициэнт корреляции Пирсона,
построим распределение данных в зависимости от вероятности ухода
сравним медианные или средние значения исследуемого параметра в зависимости от вероятности ухода.
df['score'].corr(df['churn']).round(2)
0.1
fig = px.histogram(df, x="score", color="churn", marginal="rug",
hover_data=df.columns, title='Распределение кредитного рейтинга в зависимости от оттока клиентов')
fig.update_layout(xaxis_title='Кредитный рейтинг')
fig.show()
df.pivot_table(index = 'churn', values = 'score', aggfunc = 'median').reset_index()
| churn | score | |
|---|---|---|
| 0 | 0 | 847 |
| 1 | 1 | 866 |
Кредитный скоринг остающихся клиентов более равномерно распределён от 0 до 1000, кредитный скоринг уходящих клиентов начинается от 700 и максимально сосредоточен в области 800-930.
У уходящих клиентов медианный кредитный скоринг 866, у остающихся 847.
specs = [[{'type':'domain'}, {'type':'domain'}, {'type': 'domain'}]]
colors = ['rgb(33, 75, 99)', 'rgb(79, 129, 102)']
fig = make_subplots(rows=1, cols=3, specs=specs)
# Define pie charts
fig.add_trace(go.Pie(labels=df['churn'], values=df['city_Ярославль'], name='Ярославль',
marker_colors=colors, scalegroup='one', title='Ярославль'), 1, 1)
fig.add_trace(go.Pie(labels=df['churn'], values=df['city_Рыбинск'],name='Рыбинск',
marker_colors=colors, scalegroup='one', title= 'Рыбинск'), 1, 2)
fig.add_trace(go.Pie(labels=df['churn'], values=df['city_Ростов'], name='Ростов',
marker_colors=colors, scalegroup='one', title= 'Ростов'), 1, 3)
fig.update(layout_title_text='Соотношение уходящих и не уходящих клиентов по городам',
layout_showlegend=True)
fig = go.Figure(fig)
fig.show()
Больше всего клиентов уходит в Ярославле ( как в количественном выражении, так и в процентном) 19,2% - 1094 чел.
df_gender1 = df.pivot_table(index = 'gender_Ж', values = 'churn', aggfunc = 'sum').reset_index()
df_gender1
| gender_Ж | churn | |
|---|---|---|
| 0 | 0 | 1161.0 |
| 1 | 1 | 631.0 |
Отток мужчин в 2 раза больше, чем женщин.
Посмотрим сколько мужчин и женщин вообще обслуживается в банке
df_gender = df.groupby(['gender_Ж'])[['gender_Ж']].count().rename(columns={'gender_Ж': 'count'}).reset_index()
df_gender
| gender_Ж | count | |
|---|---|---|
| 0 | 0 | 4878 |
| 1 | 1 | 4859 |
specs = [[{'type':'domain'}, {'type':'domain'}]]
colors = ['rgb(129, 180, 179)', 'rgb(124, 103, 37)']
fig = make_subplots(rows=1, cols=2, specs=specs)
fig.add_trace(go.Pie(labels=df_gender1['gender_Ж'], values=df_gender1['churn'], name='Ушедшие клиенты',
marker_colors=colors, title='Ушедшие клиенты. 0- мужчины, 1- женщины', textinfo='label+percent'), 1, 1)
fig.add_trace(go.Pie(labels=df_gender['gender_Ж'], values=df_gender['count'],name='0- мужчины, 1- женщины',
marker_colors=colors, title= 'Все клиенты. 0- мужчины, 1- женщины', textinfo='label+percent'), 1, 2)
fig.update(layout_title_text='Зависимость оттока клиентов от пола',
layout_showlegend=True)
fig = go.Figure(fig)
fig.show()
В общей выборке клиенты по половому признаку делятся практически пополам. В категории ушедших клиентов мужчин почти в 2 раза больше чем женщинЖ (64,8% мужчин и 35,2% женщин)
Для определения зависимости баллов кредитного скоринга (кредитный рейтинг) и вероятности уходя сделаем следующие действия:
посчитаем коэфициэнт корреляции Пирсона,
построим распределение данных в зависимости от вероятности ухода
сравним медианные или средние значения исследуемого параметра в зависимости от вероятности ухода.
df['age'].corr(df['churn']).round(2)
-0.05
fig = px.histogram(df, x="age", color="churn", marginal="rug",
hover_data=df.columns, title='Распределение возраста в зависимости от оттока клиентов')
fig.update_layout(xaxis_title='Возраст')
fig.show()
Распределение не симметрично, поэтому для сравнения "среднего" возраста ушедших и не ушедших клиентов будем использовать медиану
df.pivot_table(index = 'churn', values = 'age', aggfunc = 'median')
| age | |
|---|---|
| churn | |
| 0 | 40 |
| 1 | 39 |
Сильной разницы в возрасте между отточными и не отточными клиентами не наблюдается. Самый частый возраст оттока с 30 до 40 лет
df['equity'].corr(df['churn']).round(2)
0.26
df_no_churn = df.loc[df['churn']==0].groupby(['equity'])[['churn']].count()\
.rename(columns={'churn': 'count'}).reset_index().sort_values(by='count', ascending= False)
df_no_churn
| equity | count | |
|---|---|---|
| 0 | 0 | 2348 |
| 4 | 4 | 1363 |
| 5 | 5 | 1320 |
| 3 | 3 | 1204 |
| 2 | 2 | 879 |
| 1 | 1 | 670 |
| 6 | 6 | 102 |
| 7 | 7 | 43 |
| 8 | 8 | 10 |
| 9 | 9 | 6 |
df_churn = df.loc[df['churn']==1].groupby(['equity'])[['churn']].count()\
.rename(columns={'churn': 'count'}).reset_index().sort_values(by='count', ascending= False)
df_churn
| equity | count | |
|---|---|---|
| 5 | 5 | 560 |
| 4 | 4 | 461 |
| 3 | 3 | 317 |
| 2 | 2 | 166 |
| 1 | 1 | 93 |
| 0 | 0 | 88 |
| 6 | 6 | 58 |
| 7 | 7 | 36 |
| 9 | 9 | 7 |
| 8 | 8 | 6 |
specs = [[{'type':'domain'}, {'type':'domain'}]]
colors = ['rgb(129, 180, 179)', 'rgb(124, 103, 37)']
fig = make_subplots(rows=1, cols=2, specs=specs)
fig.add_trace(go.Pie(labels=df_churn['equity'], values=df_churn['count'], name='Ушедшие клиенты',
marker_colors=colors, title='Ушедшие клиенты', textinfo='label+percent'), 1, 1)
fig.add_trace(go.Pie(labels=df_no_churn['equity'], values=df_no_churn['count'],name='Оставшиеся клиенты',
marker_colors=colors, title= 'Оставшиеся', textinfo='label+percent'), 1, 2)
fig.update(layout_title_text='Распледеление клиентов в зависимости от оценки собственности среди ушедших и оставшихся',
layout_showlegend=True)
fig = go.Figure(fig)
fig.show()
Среди ушедших клиентов наибольшее количество 31% имели оценку собственности 5,на втором месте 26% -4. Среди оставшихся немного иное распределение: наибольшее количество имеют оценку 0 - 30%, на втором месте также оценка 4 - 17,2%
Посмотрим соотношение оставшихся и ушедших клиентов в зависимости от оценки собственности.
df_group=df.groupby(['equity', 'churn'])[['userid']].count().rename(columns={'userid': 'count'}).reset_index()
#добавим проценты
df_group['ratio'] = (df_group['count']/df_group.groupby('equity')['count'].transform('sum')).round(2)*100
df_group
| equity | churn | count | ratio | |
|---|---|---|---|---|
| 0 | 0 | 0 | 2348 | 96.0 |
| 1 | 0 | 1 | 88 | 4.0 |
| 2 | 1 | 0 | 670 | 88.0 |
| 3 | 1 | 1 | 93 | 12.0 |
| 4 | 2 | 0 | 879 | 84.0 |
| 5 | 2 | 1 | 166 | 16.0 |
| 6 | 3 | 0 | 1204 | 79.0 |
| 7 | 3 | 1 | 317 | 21.0 |
| 8 | 4 | 0 | 1363 | 75.0 |
| 9 | 4 | 1 | 461 | 25.0 |
| 10 | 5 | 0 | 1320 | 70.0 |
| 11 | 5 | 1 | 560 | 30.0 |
| 12 | 6 | 0 | 102 | 64.0 |
| 13 | 6 | 1 | 58 | 36.0 |
| 14 | 7 | 0 | 43 | 54.0 |
| 15 | 7 | 1 | 36 | 46.0 |
| 16 | 8 | 0 | 10 | 62.0 |
| 17 | 8 | 1 | 6 | 38.0 |
| 18 | 9 | 0 | 6 | 46.0 |
| 19 | 9 | 1 | 7 | 54.0 |
fig = px.bar(df_group, x='equity', y='ratio', color = 'churn',\
title='Зависимость оттока клиентов от оценки собственности клиента')
fig.update_layout(xaxis_title='Оценка собственности',
yaxis_title='Доля клиентов, %')
fig.show()
Наибольшая доля ушедших клиенто приходится на оценку собственности - 9. Заметна тенденция увеличения доли ушедших клиентов у увеличением оценки собственности (оценка 8 выбивается из немного из этой тенденции)
На мой взгляд зависимоть от оценки собственности есть. С увеличением оценки увеличивается отток.Об этом говорит последний график, коэфициент Пирсона тоже. Конечно клиентов с оценкой собственности больше 7 не так много, чтобы делать однозначный вывод, но по крайней мере до 7 - тенденция явно просматривается.
df['balance'].corr(df['churn']).round(2)
0.2
fig = px.histogram(df, x="balance", color="churn", marginal="rug",
hover_data=df.columns, title='Распределение баланса счёта в зависимости от оттока клиентов')
fig.update_layout(xaxis_title='Баланс, руб')
fig.show()
df_balance = df.pivot_table(index = 'churn', values = 'balance', aggfunc = 'median').reset_index()
df_balance
| churn | balance | |
|---|---|---|
| 0 | 0 | 471966.695 |
| 1 | 1 | 770771.685 |
fig = px.bar(df_balance, x='churn', y='balance', \
title='Зависимость оттока от баланса')
fig.update_layout(xaxis_title='1: клиент ушёл, 0: клиент остаётся',
yaxis_title='Баланс, руб')
fig.show()
В среднем у уходящих клиентов баланс счета выше, чем у остающихся. Наибольшее количество ушедших клиентов имеет баланс от 70 до 900 тыс
df['products'].corr(df['churn']).round(2)
0.3
df_products=df.groupby(['products', 'churn'])[['userid']].count().rename(columns={'userid': 'count'}).reset_index()
#добавим проценты
df_products['ratio'] = (df_products['count']/df_products.groupby('products')['count'].transform('sum')).round(2)*100
df_products
| products | churn | count | ratio | |
|---|---|---|---|---|
| 0 | 1 | 0 | 2960 | 93.0 |
| 1 | 1 | 1 | 230 | 7.0 |
| 2 | 2 | 0 | 4061 | 81.0 |
| 3 | 2 | 1 | 964 | 19.0 |
| 4 | 3 | 0 | 731 | 71.0 |
| 5 | 3 | 1 | 294 | 29.0 |
| 6 | 4 | 0 | 172 | 37.0 |
| 7 | 4 | 1 | 295 | 63.0 |
| 8 | 5 | 0 | 21 | 70.0 |
| 9 | 5 | 1 | 9 | 30.0 |
fig = px.bar(df_products, x='products', y='ratio', color = 'churn',\
title='Зависимость оттока клиентов от количества используемых продуктов')
fig.update_layout(xaxis_title='Количество продуктов',
yaxis_title='Доля клиентов, %')
fig.show()
Доля уходящих клиентов увеличивается при увеличении количества продуктов от 1 до 4, но если клиент начинает пользоваться 5-ю продутками, то вероятность что он лстанется уведичивается. Наибольшая доля оттока у клиентов пользующихся 4-мя продуктами.
df_products.loc[df_products['churn']==1]
| products | churn | count | ratio | |
|---|---|---|---|---|
| 1 | 1 | 1 | 230 | 7.0 |
| 3 | 2 | 1 | 964 | 19.0 |
| 5 | 3 | 1 | 294 | 29.0 |
| 7 | 4 | 1 | 295 | 63.0 |
| 9 | 5 | 1 | 9 | 30.0 |
fig = px.bar(df_products.loc[df_products['churn']==1], x='products', y='count', \
title='Зависимость оттока клиентов от количества используемых продуктов')
fig.update_layout(xaxis_title='Количество продуктов',
yaxis_title='Количество ушедших клиентов')
fig.show()
Если смотреть по абсолютному количеству, максимальный отток наблюдается у пользователей с 2-мя продуктами.Но этих пользователей в принципе больше. Думаю, что всё же корректнее смотреть на долю, а не на абсолютное значение.
df.pivot_table(index = 'credit_card', values = 'churn', aggfunc = 'sum').reset_index()
| credit_card | churn | |
|---|---|---|
| 0 | 0 | 804.0 |
| 1 | 1 | 988.0 |
Среди ушедших клиентов чуть больше половины имели кредитные карты.
df_credit = df.groupby(['credit_card'])[['credit_card']].count().rename(columns={'credit_card': 'count'}).reset_index()
df_credit
| credit_card | count | |
|---|---|---|
| 0 | 0 | 3128 |
| 1 | 1 | 6609 |
specs = [[{'type':'domain'}, {'type':'domain'}]]
colors = ['rgb(129, 180, 179)', 'rgb(124, 103, 37)']
fig = make_subplots(rows=1, cols=2, specs=specs)
fig.add_trace(go.Pie(labels=df['credit_card'], values=df['churn'], name='Ушедшие клиенты',
marker_colors=colors, title='Ушедшие клиенты', textinfo='label+percent'), 1, 1)
fig.add_trace(go.Pie(labels=df_credit['credit_card'], values=df_credit['count'],name='Все клиенты',
marker_colors=colors, title= 'Все клиенты', textinfo='label+percent'), 1, 2)
fig.update(layout_title_text='Зависимость оттока клиентов от наличия кредитной карты. 1- есть карта, 0- нет карты',
layout_showlegend=True)
fig = go.Figure(fig)
fig.show()
Доля клиентов с кредитными картами среди ушедших клиентов меньше, чем среди общего количества клиентов на 12 %.
Склоняюсь, что зависимость есть, хоть и небольшая. Отрицательный коэффициент корреляции тоже говорит об обратной связи. Логически если ты платишь кредит, то ты просто не можешь уйти из банка, пока не погасишь его.
df['last_activity'].corr(df['churn']).round(2)
0.17
df_activity = df.pivot_table(index = 'last_activity', values = 'churn', aggfunc = 'sum').reset_index()
df_activity
| last_activity | churn | |
|---|---|---|
| 0 | 0 | 539.0 |
| 1 | 1 | 1253.0 |
fig = px.bar(df_activity, x='last_activity', y='churn',\
title='Зависимость оттока клиентов от активности')
fig.update_layout(xaxis_title='1: активный клиент, 0: неактивный',
yaxis_title='Количество ушедших клиентов, руб')
fig
fig.show()
Как это ни странно, среди ушедших пользователей активных было больше чем не автивных больше чем в 2 раза.
Посмотрим доли доли ушедших пользователей среди общего количества с разбивкой по активности.
df_activity1=df.groupby(['last_activity', 'churn'])[['userid']].count().rename(columns={'userid': 'count'}).reset_index()
#добавим проценты
df_activity1['ratio'] = (df_activity1['count']/df_activity1.groupby('last_activity')['count'].transform('sum')).round(2)*100
df_activity1
| last_activity | churn | count | ratio | |
|---|---|---|---|---|
| 0 | 0 | 0 | 4114 | 88.0 |
| 1 | 0 | 1 | 539 | 12.0 |
| 2 | 1 | 0 | 3831 | 75.0 |
| 3 | 1 | 1 | 1253 | 25.0 |
fig = px.bar(df_activity1, x='last_activity', y='ratio', color = 'churn',\
title='Зависимость оттока клиентов от активности пользователей. churn: 0 - оставшиеся, 1- ушедшие клиенты')
fig.update_layout(xaxis_title='Активность клиента: 1-активный, 0 - неактивный',
yaxis_title='Доля клиентов, %')
fig.show()
Доля ушедших клиентов среди активных пользователей больше примерно на 20%
fig = px.histogram(df, x="est_salary", color="churn", marginal="rug",
hover_data=df.columns, title='Распределение предполагаемого дохода в зависимости от оттока клиентов')
fig.update_layout(xaxis_title='Предполагаемый доход, руб')
fig.show()
df_salary = df.pivot_table(index = 'churn', values = 'est_salary', aggfunc = 'median').reset_index()
df_salary
| churn | est_salary | |
|---|---|---|
| 0 | 0 | 116336.630 |
| 1 | 1 | 123792.985 |
fig = px.bar(df_salary, x='churn', y='est_salary',\
title='Зависимость оттока клиентов от дохода')
fig.update_layout(xaxis_title='Отток: 1-ушёл, 0 - остался',
yaxis_title='Медианный доход, руб')
fig
fig.show()
Распределение не симметрично, для сравнения посчитали медианы. Есть небольшое различие. У ушедших клиентов медианный доход немного больше, чем у оставшихся.
Выделим несколько сегментов, в которых как мы думаем высока доля ушедших клиентов и посмотрим какой процентоттока получится при сочетании этих сегментов. Посчитаем 3 варианта:
Мужчины с оценкой собственности 7 и выше и балансом больше 400 тыс
Возраст 30-40, пользуются 4-мя продуктами, кредитный рейтинг больше 800
Зарплата выше 70тыс, активные пользователи, пользуются 4-мя продуктами
df_seg1 = df.loc[(df['gender_М']== 1) & (df['balance'] > 400000) & (df['equity'] >= 7)]
df_seg1 = df_seg1.groupby(['churn'])[['userid']].count().rename(columns={'userid': 'count'}).reset_index()
df_seg1['ratio'] = (df_seg1['count']/(df_seg1['count'].sum())).round(2)*100
df_seg1
| churn | count | ratio | |
|---|---|---|---|
| 0 | 0 | 19 | 40.0 |
| 1 | 1 | 28 | 60.0 |
df_seg2 = df.loc[(df['age'] > 30 ) & (df['age'] < 40) & (df['score'] > 800) &(df['products'] ==4)]
df_seg2 = df_seg2.groupby(['churn'])[['userid']].count().rename(columns={'userid': 'count'}).reset_index()
df_seg2['ratio'] = (df_seg2['count']/(df_seg2['count'].sum())).round(2)*100
df_seg2
| churn | count | ratio | |
|---|---|---|---|
| 0 | 0 | 69 | 37.0 |
| 1 | 1 | 116 | 63.0 |
df_seg3 = df.loc[(df['est_salary'] > 70000 ) & (df['last_activity'] == 1) &(df['products'] ==4)]
df_seg3 = df_seg3.groupby(['churn'])[['userid']].count().rename(columns={'userid': 'count'}).reset_index()
df_seg3['ratio'] = (df_seg3['count']/(df_seg3['count'].sum())).round(2)*100
df_seg3
| churn | count | ratio | |
|---|---|---|---|
| 0 | 0 | 88 | 33.0 |
| 1 | 1 | 176 | 67.0 |
specs = [[{'type':'domain'}, {'type':'domain'}, {'type': 'domain'}]]
colors = ['rgb(33, 75, 99)', 'rgb(79, 129, 102)']
fig = make_subplots(rows=1, cols=3, specs=specs)
# Define pie charts
fig.add_trace(go.Pie(labels=df_seg1['churn'], values=df_seg1['count'], name='Первый вариант сегментации',
marker_colors=colors, title='Вариант 1'), 1, 1)
fig.add_trace(go.Pie(labels=df_seg2['churn'], values=df_seg2['count'], name='Второй вариант сегментации',
marker_colors=colors, title='Вариант 2'), 1, 2)
fig.add_trace(go.Pie(labels=df_seg3['churn'], values=df_seg3['count'], name='Третий вариант сегментации',
marker_colors=colors, title='Вариант 3'), 1, 3)
fig.update(layout_title_text='Анализ вероятности оттока в зависимости от разных критериев (сегментаций)',
layout_showlegend=True)
fig = go.Figure(fig)
fig.show()
Наибольший процент оттока в третьем варианте сегментации: зарплата выше 70 тыс, пользующихся 4-мя продуктами и имеющие оценку собственности выше 7.
Я пробовала разные сочетания параметров. В итоге наибольшее влияние оказывает то, что клиент пользуется 4-мя продуктами банка.
Согласно проведённым исследованиям, чаще уходят из банка:
Мужчины в возрасте 30-40 лет
С зарплатой больше 70 тыс. руб
Пользующиеся 4-мя продуктами
Имеющие высокий кредитный скор от 800
Активный пользователь банка
С высокой оценкой собственности (выше 6)
Одним словом уходят успешные, активные клиенты.
Наибольшее влияние на отток клиентов оказывают следующие параметры:
пользование 4-мя продуктами: доля уходящих клиентов увеличивается при увеличении количества продуктов от 1 до 4, но если клиент начинает пользоваться 5-ю продутками, то вероятность что он уйдёт уменьшится. Наибольшая доля оттока у клиентов пользующихся 4-мя продуктами ~63%.
активность: доля ушедших клиентов среди активных пользователей больше примерно на 20%
гендерная принадлежность к мужскому полу: ~65% отточных клиентов мужчины
кредитный рейтинг выше 800: кредитный скоринг остающихся клиентов более равномерно распределён от 0 до 1000, кредитный скоринг уходящих клиентов начинается от 700 и максимально сосредоточен в области 800-930. У уходящих клиентов медианный кредитный скоринг 866, у остающихся 847
оценка собвственности: с увеличением оценки увеличивается отток. Наибольшая доля ушедших клиентов приходится на оценку собственности - 9-54%
имеют медианный баланс выше чем неотточные клиенты: медианный баланс отточных клиентов ~770тыс. руб., остающихся ~472 тыс. руб.,
имеют медианный доход выше чем неотточные клиенты: медианный доход отточных клиентов ~124тыс. руб., остающихся ~116 тыс. руб.,
Параметры, которые меньше влияют на отток клиентов:
Город. По всем 3 городам примерно одинаковый % оттока клиентов (от 16 до 19%%)
Наличие кредитной карты: доля клиентов с кредитными картами среди ушедших клиентов меньше, чем среди общего количества клиентов на 12
Рекомендации:
Провести более детальный анализ влияния пользования продуктами банка на отток клиентов. По логике, чем большим количеством продуктов пользуется клиент, тем он более лоялен к банку. У нас получается наоборот.
Сформировать выгодные предложения и продукты, ориентированные на портрет отточных клиентов - успешных, активных людей.
Гипотеза 1. Средние доходы пользователей, которые ушли и остались, различаются
Гипотеза 2. Активность пользователей, которые ушли и остались, различаются
Гипотеза 3. Количество продуктов, которыми пользовались ушедшие и оставшиеся клиенты различаются
H_0: Доход клиентов, которые ушли = доход клиентов, которые остались
H_1: Доход клиентов, которые ушли ≠ доход клиентов, которые остались
alpha = 0.05
fig = px.histogram(df, x="est_salary", color="churn", marginal="rug",
hover_data=df.columns, title='Распределение предполагаемого дохода в зависимости от оттока клиентов')
fig.update_layout(xaxis_title='Предполагаемый доход, руб')
fig.show()
Чтобы мы могли применять для расчётов t-критерий Стьюдента необходимо, чтобы наши данные были распределены нормально, чтобы выборки были не зависимы друг от друга и дисперсии рассматриваемых генеральных совокупностей были.
Наши данные соответствуют всем условиям, кроме последнего. В равенстве дисперсии мы не можем быть уверены, и поскольку и по количеству данных выборки сильно отличаются, в расчёте мы укажем параметр equal_var = False
churn_1 = df[df['churn'] == 1]['est_salary']
churn_0 = df[df['churn'] == 0]['est_salary']
results = st.ttest_ind(churn_1, churn_0, equal_var = False) # results = вызов метода для проверки гипотезы
alpha = 0.05
print(results.pvalue)
if results.pvalue < alpha:
print('Отвергаем нулевую гипотезу')
else:
print('Не получилось отвергнуть нулевую гипотезу')
0.0007134767724212125 Отвергаем нулевую гипотезу
Средние доходы оставшихся и ушедших клиентов не равны
H_0: Активность клиентов, которые ушли = активности клиентов, которые остались
H_1: Активность клиентов, которые ушли ≠ активности клиентов, которые остались
alpha = 0.05
fig = px.histogram(df, x="last_activity", color="churn", marginal="rug",
hover_data=df.columns, title='Активности в зависимости от оттока клиентов')
fig.update_layout(xaxis_title='Активность: 0- не активный, 1- активный', yaxis_title = "Количество пользователей")
fig.show()
Мы имеем дело с непараметрическими данными. Поэтому будем использовать тест Манна-Уитни
churn_1 = df[df['churn'] == 1]['last_activity']
churn_0 = df[df['churn'] == 0]['last_activity']
results = st.mannwhitneyu(churn_1, churn_0)
print('p-значение: ', results.pvalue)
alpha = 0.05
if results.pvalue < alpha:
print('Отвергаем нулевую гипотезу')
else:
print(
'Не получилось отвергнуть нулевую гипотезу'
)
p-значение: 5.590492841187725e-62 Отвергаем нулевую гипотезу
Активность ушедших и оставшихся клиентов не равны
H_0: Количество продуктов клиентов, которые ушли = количество продуктов клиентов, которые остались
H_1: Количество продуктов клиентов, которые ушли ≠ количество продуктов клиентов, которые остались
alpha = 0.05
fig = px.histogram(df, x="products", color="churn", marginal="rug",
hover_data=df.columns, title='Распределение количества продуктов в зависимости от оттока клиентов')
fig.update_layout(xaxis_title='Количество продуктов', yaxis_title = "Количество пользователей")
fig.show()
Мы имеем дело с непараметрическими данными. Поэтому будем использовать тест Манна-Уитни
churn_1 = df[df['churn'] == 1]['products']
churn_0 = df[df['churn'] == 0]['products']
results = st.mannwhitneyu(churn_1, churn_0)
print('p-значение: ', results.pvalue)
alpha = 0.05
if results.pvalue < alpha:
print('Отвергаем нулевую гипотезу: разница статистически значима')
else:
print(
'Не получилось отвергнуть нулевую гипотезу, вывод о различии сделать нельзя'
)
p-значение: 1.3389821316567326e-154 Отвергаем нулевую гипотезу: разница статистически значима
Количество продуктов , которыми пользовались ушедшие клиенты отличается от количества продуктов, которыми пользуются оставшиеся клиенты
Проверка гипотез подтвердила наши более ранние выводы о различном поведении клиентов которые ушли и которые остались